<script setup>
import { useResizeObserver } from '@vueuse/core'
import ImageCard from '@/views/interestingComponents/waterfall/components/image-card.vue'
import { emitter, throttleAndDebounce } from '@/utils'

// 参考于 https://github.com/pwp-app/pixiviz/blob/master/src/components/common/Waterfall.vue

const props = defineProps({
  images: Array,
  cardWidth: {
    type: Number,
    default: 280,
  },
  squaredImage: Boolean,
  gap: {
    type: Number,
    default: 10,
  },
  rowsPerSection: {
    type: Number,
    default: 3,
  },
})

const { images, cardWidth, squaredImage, gap, rowsPerSection } = toRefs(props)

const container = ref(null)
const containerWidth = ref(0)
const containerOffset = ref(0)

const maxHeight = ref(0)
const scrollTop = ref(0)

const positionMap = ref({})
const widthStore = ref({})
const heightStore = ref({})

const sections = ref([{}])
const group = ref({})
const inGroup = ref({})

const currentSectionCount = ref(0)

const displayImages = ref([])

// 组分割大小
const groupSize = 500
// 瀑布流可视区额外距离
const additionalDistance = 1000

const columns = computed(() => Math.floor((containerWidth.value + gap.value) / (cardWidth.value + gap.value)))

watch(
  images,
  () => {
    imageChange()
  },
)

watch(
  columns,
  () => {
    columnsChanged()
  },
)

const onScroll = throttleAndDebounce(handleScroll, 300)
const onResize = throttleAndDebounce(handleResize, 100)

onBeforeMount(() => {
  emitter.on('contentScroll', onScroll)
})

onMounted(() => {
  containerWidth.value = getContainerWidth()
  containerOffset.value = getContainerOffset()

  resetWidthStore()
  resetHeightStore()

  useResizeObserver(container, onResize)
})

onUnmounted(() => {
  emitter.off('contentScroll', onScroll)
})

function handleResize() {
  containerWidth.value = getContainerWidth()
}

function handleScroll(e) {
  containerOffset.value = getContainerOffset()
  scrollTop.value = e.target.scrollTop - containerOffset.value
  setDisplay()
}

function getContainerWidth() {
  return container?.value.offsetWidth ?? 0
}

function getContainerOffset() {
  if (!container.value)
    return 0

  return container.value.offsetTop
}

function getLoadHeight(image) {
  return Math.floor(image.height / (image.width / cardWidth.value))
}

function getMinHeightCol() {
  let min = Number.MAX_VALUE
  let minIndex = 0

  for (let i = 0; i < columns.value; i++) {
    if (heightStore.value[i] < min) {
      min = heightStore.value[i]
      minIndex = i
    }
  }

  return minIndex
}

function imageChange() {
  computePosition()
  setMaxHeight()
  setDisplay()
}

function columnsChanged() {
  renderWaterfall()
}

function computePosition() {
  for (const [i, image] of images.value.entries()) {
    if (positionMap.value[i])
      continue

    const countPerSection = columns.value * rowsPerSection.value

    const h = squaredImage.value ? cardWidth : getLoadHeight(image)

    image.index = i

    let left = 0
    let top = 0
    let storeIdx = 0

    if (i < columns.value) {
      left = widthStore.value[i]
      top = 0
      storeIdx = i
    }
    else {
      const minHeightIdx = getMinHeightCol()
      left = widthStore.value[minHeightIdx]
      top = heightStore.value[minHeightIdx]
      storeIdx = minHeightIdx
    }

    positionMap.value[i] = { height: h }
    positionMap.value[i].left = left
    positionMap.value[i].top = top
    heightStore.value[storeIdx] += h + gap.value

    if (currentSectionCount.value < countPerSection) {
      currentSectionCount.value += 1
    }
    else {
      currentSectionCount.value = 1
      sections.value.push({})
    }

    const sectionIdx = sections.value.length - 1
    const { head, tail } = sections.value[sectionIdx]

    if (typeof head === 'undefined' || top < head)
      sections.value[sectionIdx].head = top

    const bottom = top + h
    if (typeof tail === 'undefined' || bottom > tail)
      sections.value[sectionIdx].tail = bottom
  }

  for (const [idx, section] of sections.value.entries()) {
    if (inGroup.value[idx])
      continue

    const { head, tail } = section

    if (typeof head === 'undefined' || typeof tail === 'undefined')
      return

    const start = Math.floor(head / groupSize)
    const end = Math.floor(tail / groupSize)

    for (let i = start; i <= end; i++) {
      if (!group.value[i])
        group.value[i] = []

      group.value[i].push(idx)
    }

    inGroup.value[idx] = true
  }
}

function getMaxHeight() {
  return Math.max(...Object.values(heightStore.value))
}

function setMaxHeight() {
  maxHeight.value = getMaxHeight()
}

function setDisplay() {
  const countPerSection = rowsPerSection.value * columns.value
  const showCondHead = scrollTop.value - additionalDistance
  const showCondTail = scrollTop.value + document.querySelector('.layout-content').offsetHeight + additionalDistance
  const start = Math.floor(showCondHead / groupSize)
  const end = Math.floor(showCondTail / groupSize)

  let list = []
  const inList = {}

  const processor = (idx) => {
    if (inList[idx])
      return

    list = list.concat(images.value.slice(idx * countPerSection, (idx + 1) * countPerSection))
    inList[idx] = true
  }

  for (let i = start; i <= end; i++) {
    if (!group.value[i])
      continue

    group.value[i].forEach(processor)
  }

  if (window.requestAnimationFrame) {
    window.requestAnimationFrame(() => {
      displayImages.value = list
    })
  }
  else {
    displayImages.value = list
  }
}

function renderWaterfall() {
  resetGroup()
  resetSections()
  resetWidthStore()
  resetHeightStore()
  resetPositionMap()
  setMaxHeight()
  setDisplay()
}

function resetSections() {
  sections.value = [{}]
  currentSectionCount.value = 0
}

function resetGroup() {
  group.value = {}
  inGroup.value = {}
}

function resetWidthStore() {
  widthStore.value = {}

  for (let i = 0; i < columns.value; i++)
    widthStore.value[i] = (cardWidth.value + gap.value) * i
}

function resetHeightStore() {
  heightStore.value = {}

  for (let i = 0; i < columns.value; i++)
    heightStore.value[i] = 0
}

function resetPositionMap() {
  positionMap.value = {}
  computePosition()
}
</script>

<template>
  <div ref="container" :style="{ height: `${maxHeight}px` }" class="waterfall-container">
    <ImageCard
      v-for="({ index }) of displayImages"
      :key="index"
      :card-width="cardWidth"
      :position="positionMap[index]"
      class="waterfall-item"
    >
      {{ index }}
    </ImageCard>
  </div>
</template>

<style lang="scss" scoped>
.waterfall-container {
  @apply relative;
}
</style>
